<?php

    /**********************************************************************************
     * LeafPHP PDO数据库驱动层 www.leafphp.com
     * --------------------------------------------------------------------------------
     * pdo驱动层，单例模式，可单独提取出来作为其他项目的pdo方式的db类（需要设置一个顶级异常处理器并替换L()函数）
     * 1、根据操作方式，自动选择合适的数据源
     * 2、支持mysql集群模式、读写分离模式
     * 3、非ORM模式的PDO DB类
     * 4、可操作方式
     * --------------------------------------------------------------------------------
     * 操作变量范例：
     * 说明：$pdoMy为当前对象，该对象可通过$pdoMy=pdoMy::getInstance()获取。在本框架中，在控制器中，用$this->getDb()获取
     * $sql 对应如：insert into leaf_user(name) values(':name')
     * $data对应如：array(':name'=>'lisi');
     * --------------------------------------------------------------------------------
     * 非连贯方式操作
     * 插入操作：$pdoMy->insert($sql,$data);
     * 更新操作：$pdoMy->update($sql,$data);
     * 删除操作：$pdoMy->delete($sql,$data);
     * 获取多行结果集：$pdoMy->fetchAll($sql, $data)
     * 获取单行结果集：$pdoMy->fetchRow($sql, $data)
     * 获取单行中的第一个字段：$pdoMy->fetchColumn($sql, $data)
     * --------------------------------------------------------------------------------
     * 连贯方式操作
     * 插入操作：$pdoMy->prepare($sql)->execute($data)->insert();
     * 更新操作：$pdoMy->prepare($sql)->execute($data)->update();
     * 删除操作：$pdoMy->prepare($sql)->execute($data)->delete();
     * 批量插入操作：$pdoMy->prepare($sql); 多次$pdoMy->execute($data);
     * 获取多行结果集：$pdoMy->prepare($sql)->execute($data)->fetchAll()
     * 获取单行结果集：$pdoMy->prepare($sql)->execute($data)->fetchRow()
     * 获取单行中的第一个字段：$pdoMy->prepare($sql)->execute($data)->fetchColumn()
     * --------------------------------------------------------------------------------
     * 事务处理操作
     * 事务操作1：try{$pdoMy->transaction();各种操作（连贯方式或非连贯方式均可）....; $pdoMy->commit() }
     * 事务操作2，回滚：try{$pdoMy->transaction();各种操作....;手动抛出异常（$pdoMy->exception('异常')） $pdoMy->commit() }，此种方式无需捕获异常，即可实现回滚
     * 事务操作3，回滚：try{$pdoMy->transaction();各种操作....;手动抛出异常 （throw new exception('异常')） $pdoMy->commit() }catch($pdoMy->rollBack())，此种方式比较传统，要想回滚，必须将$pdoMy->rollBack()放置于catch块中。
     * 事务操作注意：try块中的代码必须是同一个数据源，也就是同一个$pdoMy，否则如果为分布式数据库，将出现操作操作不为同一个数据库的现象。
     * --------------------------------------------------------------------------------
     * 存储过程的操作
     * 存储过程1：不获取数据的，如插入：$sql = 'call addName(\'nimei\')'; $res = $pdoMy->prepare($sql)->execute();
     * 存储过程2：获取数据的，两种方式（区别只是连贯或非连贯）：$sql = 'call getNames(300)'; $res = $pdoMy->fetchAll($sql); 或者 $res = $pdoMy->prepare($sql)->execute()->fetchAll();
     * --------------------------------------------------------------------------------
     * $Author:hongshuaidong
     * $Dtime:2014-02-05
     *********************************************************************************/

    if (!defined('__IS_LEAF__')) exit('Access Denied!');

    class pdoMy {

        static  private $thisInstance   =   null;            //单例模式实例化本对象
        static  private $pdoInstences   =   null;            //实例化的pdo集合
        static  private $dbConfig       =   null;            //数据库配置
        static  private $dbType         =   'mysql';         //数据库类型
        static  private $dbStats        =   true;            //是否开启数据库操作统计
        
        private         $pdo            =   null;            //用于连贯操作的PDO对象
        private         $stmt           =   null;            //用于连贯操作的预处理对象
        private         $continuous     =   false;           //是否为连贯操作
        public          $transaction    =   0;               //是否开启事务
        private         $rollBack       =   0;               //事务回滚标志，如果开启，则不进行curd不再进行
        private         $sql            =   '';              //sql
        private         $data           =   array();         //

        final private function __construct(){
            //防止外部实例化
            self::$dbStats = C('db_detect');
        }

        /**
         * 单例模式入口
         * @access public
         * 
         */
        static final public function getInstance(){
 
            //如果该pdo对象连接存在，则直接返回，否则，实例化一个pdo对象
            if( isset( self::$thisInstance ) ){
                return self::$thisInstance;
            }else{
                self::$thisInstance = new self();
                return self::$thisInstance;
            }

        }

        /**
         * 链接PDO，并返回pdo对象
         * @access private
         * @param  array  $dsnInfo pdo的数据源，包含着主机地址、数据库名，端口号
         * @return object $pdo 返回当前实例化后的PDO对象，如果失败，则抛出异常，当前类的异常抛出，实际上都由错误处理类的顶级异常处理器来自动处理，无须手动捕获。
         */
        final private function dbConnect($dsnInfo){

            try{
                //不同数据库的dsn格式不同，此处为多类型数据库提供可选项
                switch (self::$dbType) {
                    
                    case 'mysql':
                        //拼装dsn
                        $dsn = 'mysql:host='.$dsnInfo['db_host'].';dbname='.$dsnInfo['db_name'].';port='.$dsnInfo['db_port'];
                        if( empty($dsnInfo['db_host']) || empty($dsnInfo['db_name']) ) throw new Exception( L('pdo_connect_error') );
                        
                        //如果用户配置没有设置下面三个，则取默认值
                        $dsnInfo['db_charset']    = empty($dsnInfo['db_charset']) ? 'UTF8' : $dsnInfo['db_charset'];
                        $dsnInfo['db_persistent'] = empty($dsnInfo['db_persistent']) ? TRUE : $dsnInfo['db_persistent'];
                        $dsnInfo['db_buffered']   = empty($dsnInfo['db_buffered']) ? TRUE : $dsnInfo['db_buffered'];
                        
                        $dbOptions = array(
                            PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES \''.$dsnInfo['db_charset'].'\'',   //设置字符集
                            PDO::ATTR_PERSISTENT         =>  $dsnInfo['db_persistent'],
                            PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,                       //设置错误处理模式为异常
                            PDO::ATTR_CASE               => PDO::CASE_LOWER,                              //转换列为大写模式
                            PDO::ATTR_AUTOCOMMIT         => 1,                                            //关闭自动提交,对PDO处理事务有效
                            PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => $dsnInfo['db_buffered'],                //是否开启pdo缓冲区
                        );
                        $pdo = new PDO( $dsn, $dsnInfo['db_user'], $dsnInfo['db_pass'], $dbOptions );
                        break;
                        
                    default:
                        # code...
                        break;
                }
                return $pdo;

            }catch(PDOException $e){
                throw $e;
            }

        }

        /**
         * 更具当前数据库配置和当前操作方式，获取一个合适的pdo对象
         * 1、insert、update、delete调用数据库配置rw_mode为2或3的。
         * 2、查询操作，调用数据库配置信息中rw_mode为1的。
         * @access private
         * @param  string $method 操作方法，如insert、delete等
         * @return object $pdo 实例化的Pdo对象
         */
        final private function getOnePdoObject($method){

            /**
             * 随机一个值，直到条件符合为止，本db类是根据操作来决定使用哪个数据库信息的
             */
            $dbConfig = isset(self::$dbConfig) ? self::$dbConfig : C('db_config');
            $dbConfigCount = count($dbConfig);
            switch ($method) {
                case 'insert':
                case 'update':
                case 'delete':
                    do{
                        $num = rand(0, $dbConfigCount-1);
                    }while( !in_array( $dbConfig[$num]['rw_mode'], array(2, 3) ) );
                    $dsnInfo = $dbConfig[$num];
                    break;
                case 'select':
                    do{
                        $num = rand(0, $dbConfigCount-1);
                    }while( !in_array( $dbConfig[$num]['rw_mode'], array(1, 3) ) );
                    $dsnInfo = $dbConfig[$num];
                    break;
                default:
                    # code...
                    break;
            }
            if( isset(self::$pdoInstences[$num]) ){
                return self::$pdoInstences[$num];
            }else{
                $this->pdo = $this->dbConnect($dsnInfo);
                self::$pdoInstences[$num] = $this->pdo;
                return $this->pdo;
            }

        }


        /**
         * 设置开启事务，调用此方法之后，就可开启事务
         * @access public
         * @return object 返回当前对象
         */
        final public function transaction(){

            //获取一个写权限的数据库对象：开启事务，则直接选取有写权限的数据库对象，因为非写，对事务处理没有意义
            if( is_null($this->pdo) ) $this->getOnePdoObject('insert');
            //开启pdo对象的事务
            $this->pdo->beginTransaction();
            //设置当前对象的事务属性为打开状态
            $this->transaction = 1;
            //返回当前对象，从而支持连贯操作
            return $this;

        }

        /**
         * 用于事务的方法，抛出异常，此时，会将做事务的回滚操作
         * @access public
         * @param string $msg 抛出的消息
         */
        final public function exception( $msg = null ){

            if( !$this->transaction ) return false;
            $this->rollBack();
            if(is_null($msg)){
                 throw new Exception(L('pdo_transaction_exception'), 1);
             }else{
                 throw new Exception($msg, 1);
             }

        }

        /**
         * 设置开启事务，调用此方法之后，就可提交已经处理的事务
         * @access public
         */
        final public function commit(){

            //如果开启了事务，则进行提交操作
            if($this->transaction){
                $this->pdo->commit();
                $this->transaction = 0;
            }

        }

        /**
         * 设置开启事务，调用此方法之后，就可回滚已经处理的事务
         * @access public
         */
        final public function rollBack(){

            //如果开启了事务，触发了异常，则进行回滚操作，同时，恢复自动提交模式
            if($this->transaction){
                $this->pdo->rollBack();
                $this->transaction = 0;
                //设置回滚标志，此标志下，任何事务下的curd无法进行
                $this->rollBack = 1;
            }

        }

        /**
         * 插入操作
         * 该方法，区分连贯和非连贯操作，如果为连贯操作，说明执行到此方法，pdo->execute已执行，只需要饭后就可以了
         * @access public
         * @param  string $sql  预处理sql
         * @param  array  $data 绑定在sql中参数的值数组
         * @return int    返回被插入的记录的主键
         */
        final public function insert($sql=null, $data=null){

            try{
                //1、如果是连贯操作，说明execute方法，已经执行，此时，只需要返回最后插入的Id即可。
                if($this->continuous) return (int) $this->pdo->lastInsertId();
                //2、如果调用了$this->exception，则数据库操作不可以进行
                if($this->rollBack) return false;
                //3、如果已经存在pdo对象（此种情况为开启事务的时候，就实例化pdo对象出来了）
                if( is_null($this->pdo) ) $this->getOnePdoObject('insert');
                $this->fetchPrepare($sql, $data);
                //4、通常情况。如果不是自增id通常为整型，此处将字符串转换为整型
                return (int) $this->pdo->lastInsertId();
            }catch( PDOException $e ){
                //如果开启事务，则回滚
                if( $this->transaction ) $this->rollBack();
                throw $e;
                
            }

        }

        /**
         * 删除操作
         * @access public
         * @param  string $sql  要执行的sql预处理语句
         * @param  array  $data 绑定的参数集
         * @return int    返回被删除的行数
         */
        final public function delete($sql, $data){

            try{
                //如果是连贯操作，返回更新的记录数
                if($this->continuous) return $this->stmt->rowCount();
                //如果调用了$this->exception，则数据库操作不可以进行
                if($this->rollBack) return false;
                //如果已经存在pdo对象（此种情况为开启事务的时候，就实例化pdo对象出来了）
                if( is_null($this->pdo) )  $this->getOnePdoObject('delete');
                $this->fetchPrepare($sql, $data);
                //返回被删除的行数
                return $this->stmt->rowCount();
            }catch( PDOException $e ){
                //如果开启事务，则回滚
                if( $this->transaction ) $this->rollBack();
                throw $e;
            }

        }

        /**
         * 更新操作
         * @access public
         * @param  string $sql  要执行的sql预处理语句
         * @param  array  $data 绑定的参数集
         * @return int    返回被更新的行数
         */
        final public function update($sql, $data){
            
            try{
                //如果是连贯操作
                if($this->continuous) return $stmt->rowCount();
                //如果调用了$this->exception，则数据库操作不可以进行
                if($this->rollBack) return false;
                //如果已经存在pdo对象（此种情况为开启事务的时候，就实例化pdo对象出来了）
                if( is_null($this->pdo) ) $this->getOnePdoObject('update');
                $this->fetchPrepare($sql, $data);
                //返回被更新的行数
                return $this->stmt->rowCount();
            }catch( PDOException $e ){
                //如果开启事务，则回滚
                if( $this->transaction ) $this->rollBack();
                throw $e;
            }

        }

        /**
         * 预处理sql，此操作需要结合$this->execute操作
         * @access public
         * @param  string $sql  要执行的sql预处理语句
         * @return object $stmt 返回当前对象
         */
        final public function prepare($sql){

            //如果$mode不为null，表示用户要设置结果集的获取方式
            try{
                if( is_null($this->pdo) ) $this->getOnePdoObject('insert');
                $stmt = $this->pdo->prepare($sql);
                $stmt->setFetchMode(PDO::FETCH_ASSOC);
                //赋值预处理对象属性
                $this->stmt = $stmt;
                return $this;
            }catch(PDOException $e){
                throw $e;
            }

        }

        /**
         * 执行预处理语句，获取结果，
         * @access public
         * @param  string $data  要执行的sql预处理语句
         * @return object $this  返回当前对象
         */
        final public function execute($data=null){

            try{
                $this->continuous = true;
                //如果开启事务，且调用了$this->exception，则不执行操作
                if($this->transaction && $this->rollBack) return false;
                //执行预处理
                $this->stmt->execute($data);
                //销毁预处理对象
                //$this->stmt = null;
                return $this;
            }catch(PDOException $e){
                if( $this->transaction ) $this->rollBack();
                throw $e;
            }

        }

        /**
         * 执行预处理语句，获取结果
         * @access public
         * @param  string $data  要执行的sql预处理语句
         * @return object $this  返回当前对象
         */
        final public function fetchMode( $mode='assoc' ){

            try{
                switch ($mode) {
                    case 'assoc':
                        $mode = PDO::FETCH_ASSOC;
                        break;
                    case 'number':
                        $mode = PDO::FETCH_NUM;
                        break;
                    case 'object':
                        $mode = PDO::FETCH_OBJ;
                    default:
                        $mode = PDO::FETCH_ASSOC;
                        break;
                }
                $this->stmt->setFetchMode($mode);
                return $this;
            }catch(PDOException $e){
                throw $e;
            }

        }

        /**
         * 查询多记录操作
         * @access public
         * @param  string $sql  要执行的sql预处理语句
         * @param  array  $data 绑定的参数集
         * @return mixed    返回查询得到的结果集
         */
        final public function fetchAll($sql, $data=null, $mode=null){
            
            try{
                //如果是连贯操作
                if($this->continuous) return $this->stmt->fetchAll();
                //如果没有实例化pdo对象
                if( is_null($this->pdo) ) $this->getOnePdoObject('select');
                //##
                $this->fetchPrepare($sql, $data, $mode);
                return $this->stmt->fetchAll();
            }catch( PDOException $e ){
                throw $e;
            }

        }

        /**
         * 查询单条记录操作
         * @access public
         * @param  string $sql  要执行的sql预处理语句
         * @param  array  $data 绑定的参数集
         * @return mixed    返回查询得到的单行记录结果
         */
        final public function fetchRow($sql, $data=null, $mode=null){
            
            try{
                //如果是连贯操作
                if($this->continuous) return $this->stmt->fetch();
                if( is_null($this->pdo) ) $this->getOnePdoObject('select');
                $this->fetchPrepare($sql, $data, $mode);
                return $this->stmt->fetch();
            }catch( PDOException $e ){
                throw $e;
            }

        }

        /**
         * 查询结果集中的第一个字段的值
         * @access public
         * @param  string $sql  要执行的sql预处理语句
         * @param  array  $data 绑定的参数集
         * @return array    返回查询得到的结果集中的第一个字段，结果为一位数组
         */
        final public function fetchColumn($sql, $data=null, $mode=null){
            
            try{
                //如果是连贯操作
                if($this->continuous) return $this->stmt->fetch();
                if( is_null($this->pdo) ) $this->getOnePdoObject('select');
                $this->fetchPrepare($sql, $data, $mode);
                return $this->stmt->fetchColumn();
            }catch( PDOException $e ){
                throw $e;
            }

        }

        /**
         * 获取分页信息
         * @access public
         * @param  string $sql      要执行的sql预处理语句
         * @param  array  $data     绑定的参数集
         * @param  int    $pageSize 绑定的参数集
         * @return array  $pageInfo 返回查询得到的分页结果
         */
        final public function page($sql, $data=null, $pageSize){
            
            try{
                //如果是连贯操作
                if($this->continuous) return $this->stmt->fetch();
                if( is_null($this->pdo) ) $this->getOnePdoObject('select');
                //处理分页
                $page = $_GET['page'];

                $this->fetchPrepare($sql, $data, $mode);
                return $this->stmt->fetchColumn();
            }catch( PDOException $e ){
                throw $e;
            }

        }

        /**
         * 使用预处理，获取执行sql语句的预处理对象
         * @access private
         * @param  string $pdo  实例化的【恰当】的pdo对象
         * @param  string $sql  要执行的sql预处理语句
         * @param  array  $data 绑定的参数集
         * @param  mixed  $mode 查询模式（要获取数组型结果或对象类型的等），仅对查询有效
         * @return int    返回预处理对象
         */
        final private function fetchPrepare($sql, $data=null, $mode=null){

            //如果$mode不为null，表示用户要设置结果集的获取方式
            $this->stmt = $this->pdo->prepare($sql);
            is_null($mode) ? $this->stmt->setFetchMode(PDO::FETCH_ASSOC) : $this->stmt->setFetchMode($mode);
            $this->stmt->execute($data);
            if(self::$dbStats){
                $this->sql  = $sql;
                $this->data = $data;                
            }
            return $stmt;

        }

        /**
         * 获取最近一次执行的sql，可用于调试程序。
         * @return string $query 最近一次在数据库执行的sql语句
         */
        public function getLastSql(){

            $query  = $this->sql;
            $params = $this->data;
            $keys = array();
            $values = array();

            foreach ($params as $key=>$value){
                if (is_string($key)){
                    $keys[] = '/'.$key.'/';
                }else{
                    $keys[] = '/[?]/';
                }
                if(is_numeric($value)){
                    $values[] = intval($value);
                }else{
                    $values[] = '"'.$value .'"';
                }
            }

            $query = preg_replace($keys, $values, $query, 1, $count);
            return $query;

        }


    }

?>